﻿// Copyright 2012 Stuart Turner
// Licensed under the Apache License, Version 2.0 [http://www.apache.org/licenses/LICENSE-2.0]

using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
using System.Linq.Expressions;
using System.Text;

using Complete.Data.Linq;
using Complete.Data.Linq.Expressions;

namespace Complete.Data.Internal
{
	internal class QueryFormatter : CompleteExpressionVisitor
	{
		StringBuilder sb = new StringBuilder();
		int indent = 2;
		int depth = 0;
		bool isTopLevel = true;

		private QueryFormatter()
		{
		}

		public static FormattedExpression Format(Expression expression)
		{
			var qf = new QueryFormatter();
			expression = qf.Visit(expression);
			var select = expression as SelectExpression;
			if (select == null)
				throw new InvalidOperationException("Format did not return a select expression. What happened?");

			return new FormattedExpression()
			{
				Expression = select,
				CommandText = qf.sb.ToString(),
				ResultType = ResultType.SingleResultSet,
				Parameters = new List<DbParameter>(),
			};
		}

		private enum Indentation
		{
			Same,
			Inner,
			Outer
		}

		private void AppendNewLine(Indentation style)
		{
			sb.AppendLine();
			this.Indent(style);
			sb.Append(' ', this.depth * this.indent);
		}

		private void Indent(Indentation style)
		{
			if (style == Indentation.Inner)
			{
				this.depth++;
			}
			else if (style == Indentation.Outer)
			{
				this.depth--;
				System.Diagnostics.Debug.Assert(this.depth >= 0);
			}
		}

		protected override Expression Visit(Expression exp)
		{
			if (exp == null) return null;

			// check for supported node types first 
			// non-supported ones should not be visited (as they would produce bad SQL)
			switch (exp.NodeType)
			{
				case ExpressionType.Negate:
				case ExpressionType.NegateChecked:
				case ExpressionType.Not:
				case ExpressionType.Convert:
				case ExpressionType.ConvertChecked:
				case ExpressionType.UnaryPlus:
				case ExpressionType.Add:
				case ExpressionType.AddChecked:
				case ExpressionType.Subtract:
				case ExpressionType.SubtractChecked:
				case ExpressionType.Multiply:
				case ExpressionType.MultiplyChecked:
				case ExpressionType.Divide:
				case ExpressionType.Modulo:
				case ExpressionType.And:
				case ExpressionType.AndAlso:
				case ExpressionType.Or:
				case ExpressionType.OrElse:
				case ExpressionType.LessThan:
				case ExpressionType.LessThanOrEqual:
				case ExpressionType.GreaterThan:
				case ExpressionType.GreaterThanOrEqual:
				case ExpressionType.Equal:
				case ExpressionType.NotEqual:
				case ExpressionType.Coalesce:
				case ExpressionType.RightShift:
				case ExpressionType.LeftShift:
				case ExpressionType.ExclusiveOr:
				case ExpressionType.Power:
				case ExpressionType.Conditional:
				case ExpressionType.Constant:
				case ExpressionType.MemberAccess:
				case ExpressionType.Call:
				case ExpressionType.New:

				case (ExpressionType)CompleteExpressionType.Select:
				case (ExpressionType)CompleteExpressionType.Column:
				case (ExpressionType)CompleteExpressionType.Where:
				case (ExpressionType)CompleteExpressionType.From:
				case (ExpressionType)CompleteExpressionType.Table:
				case (ExpressionType)CompleteExpressionType.Join:
					return base.Visit(exp);

				case ExpressionType.ArrayLength:
				case ExpressionType.Quote:
				case ExpressionType.TypeAs:
				case ExpressionType.ArrayIndex:
				case ExpressionType.TypeIs:
				case ExpressionType.Parameter:
				case ExpressionType.Lambda:
				case ExpressionType.NewArrayInit:
				case ExpressionType.NewArrayBounds:
				case ExpressionType.Invoke:
				case ExpressionType.MemberInit:
				case ExpressionType.ListInit:
				default:
					throw new Exception(string.Format("The LINQ expression node of type {0} is not supported", exp.NodeType));
			}
		}

		protected override Expression VisitSelect(SelectExpression select)
		{
			var topLevel = isTopLevel;
			isTopLevel = false;

			sb.Append("select ");

			if (select.Take != null)
			{
				sb.Append("top (");
				this.Visit(select.Take);
				sb.Append(") ");
			}

			var add_comma = false;
			foreach (var c in select.Columns)
			{
				if (add_comma)
					sb.Append(", ");
				else
					add_comma = true;

				this.Visit(c.Declaration);
				sb.AppendFormat(" as [{0}]", c.Name);
			}

			this.AppendNewLine(Indentation.Same);
			sb.Append("from ");
			this.Visit(select.From);
			this.Visit(select.Where);

			// can only write order by if top level or if take != null
			if (topLevel || select.Take != null)
				// can only write order by if order by is not null
				if (select.OrderBy != null)
				{
					if (select.OrderBy.SortColumns.Count == 0 && select.OrderBy.SecondarySortingClause == null)
						throw new InvalidOperationException("How did we end up here?");

					sb.Append("order by ");
					this.VisitOrderBy(select.OrderBy);
				}

			return select;
		}

		protected override Expression VisitConditional(ConditionalExpression c)
		{
			sb.Append("case when ");
			this.Visit(c.Test);
			this.AppendNewLine(Indentation.Inner);
			sb.Append("then ");
			this.Visit(c.IfTrue);
			this.AppendNewLine(Indentation.Same);
			sb.Append("else ");
			this.Visit(c.IfFalse);
			this.AppendNewLine(Indentation.Outer);
			sb.Append("end");
			return c;
		}

		protected override Expression VisitColumn(ColumnExpression column)
		{
			sb.AppendFormat("[{0}].[{1}]", column.Alias, column.Name);
			return column;
		}

		protected override Expression VisitFrom(FromExpression from)
		{
			switch ((CompleteExpressionType)from.From.NodeType)
			{
				case CompleteExpressionType.Table:
				case CompleteExpressionType.Select:
					this.VisitSource(from.From);
					break;

				case CompleteExpressionType.Join:
					this.VisitJoin(from.From as JoinExpression);

					break;
			}
			this.AppendNewLine(Indentation.Same);
			return from;
		}

		protected override Expression VisitJoin(JoinExpression join)
		{
			VisitSource(join.Left);
			VisitJoinType(join.JoinType);
			VisitSource(join.Right);

			if (join.Comparison != null)
			{
				this.AppendNewLine(Indentation.Inner);
				sb.Append(" on ");
				this.Visit(join.Comparison);
				this.Indent(Indentation.Outer);
			}
			this.Indent(Indentation.Outer);
			return join;
		}

		protected void VisitJoinType(JoinType type)
		{
			this.AppendNewLine(Indentation.Inner);
			switch (type)
			{
				case JoinType.InnerJoin:
					sb.Append(" inner join ");
					break;
				case JoinType.LeftOuterJoin:
					sb.Append(" left outer join ");
					break;
				case JoinType.RightOuterJoin:
					sb.Append(" right outer join ");
					break;
				case JoinType.FullOuterJoin:
					sb.Append(" full outer join ");
					break;
				case JoinType.OuterApply:
					sb.Append(" outer apply ");
					break;
				case JoinType.CrossApply:
					sb.Append(" cross apply ");
					break;
				case JoinType.CrossJoin:
					sb.Append(" cross join ");
					break;
			}
		}

		protected Expression VisitSource(Expression source)
		{
			switch ((CompleteExpressionType)source.NodeType)
			{
				case CompleteExpressionType.Table:
					TableExpression table = (TableExpression)source;
					sb.Append(table.Name);
					sb.Append(" as ");
					sb.Append(QuoteString(table.Alias));
					break;
				case CompleteExpressionType.Select:
					SelectExpression select = (SelectExpression)source;
					sb.Append("(");
					this.Indent(Indentation.Inner);
					this.AppendNewLine(Indentation.Inner);
					this.VisitSelect(select);

					// new line has already been prepared, but to inner indent, so we'll move out outselves
					this.Indent(Indentation.Outer);
					sb.Remove(sb.Length - this.indent, this.indent);

					sb.Append(")");
					sb.Append(" as ");
					sb.Append(QuoteString(select.Alias));
					this.Indent(Indentation.Outer);
					break;
				case CompleteExpressionType.Join:
					this.VisitJoin(source as JoinExpression);
					break;
			}
			return source;
		}

		protected override OrderByClause VisitOrderBy(OrderByClause orderBy)
		{
			var add_comma = false;
			foreach (var col in orderBy.SortColumns)
			{
				if (add_comma)
					sb.Append(", ");
				else
					add_comma = true;

				this.Visit(col.Item1);
				if (col.Item2 == OrderByClause.SortDirection.Ascending)
					sb.Append(" asc");
				else
					sb.Append(" desc");
			}

			if (orderBy.SecondarySortingClause != null)
			{
				if (add_comma)
					sb.Append(", ");
				this.VisitOrderBy(orderBy.SecondarySortingClause);
			}
			this.AppendNewLine(Indentation.Same);
			return orderBy;
		}

		protected override Expression VisitWhere(WhereExpression where)
		{
			sb.Append("where ");

			if (where.Predicate is BinaryExpression)
				this.Visit(where.Predicate);
			else if (where.Predicate.NodeType == ExpressionType.Constant)
			{
				var value = (bool)((where.Predicate as ConstantExpression).Value);
				if (value)
					sb.Append("1 = 1");
				else
					sb.Append("0 = 1");
			}
			else if (where.Predicate is ConditionalExpression)
			{
				this.Visit(where.Predicate);
				sb.Append(" = 1");
			}
			else
				throw new NotSupportedException("Not sure yet what to do here. ");

			this.AppendNewLine(Indentation.Same);
			return where;
		}

		protected override Expression VisitBinary(BinaryExpression b)
		{
			switch (b.NodeType)
			{
				case ExpressionType.AndAlso:
				case ExpressionType.OrElse:
				case ExpressionType.LessThan:
				case ExpressionType.LessThanOrEqual:
				case ExpressionType.GreaterThan:
				case ExpressionType.GreaterThanOrEqual:
					return VisitComparison(b);

				case ExpressionType.Equal:
				case ExpressionType.NotEqual:
					return VisitEquality(b);

				case ExpressionType.Coalesce:
					return VisitCoalesce(b);

				case ExpressionType.Add:
				case ExpressionType.AddChecked:
				case ExpressionType.Subtract:
				case ExpressionType.SubtractChecked:
				case ExpressionType.Multiply:
				case ExpressionType.MultiplyChecked:
				case ExpressionType.Divide:
				case ExpressionType.Modulo:
				case ExpressionType.ExclusiveOr:
				case ExpressionType.And:
				case ExpressionType.Or:
					return VisitOperation(b);

				default:
					throw new NotSupportedException(string.Format("The binary operator '{0}' is not supported", b.NodeType));
			}
		}

		private char GetOperator(BinaryExpression b)
		{
			switch (b.NodeType)
			{
				case ExpressionType.Add:
				case ExpressionType.AddChecked:
					return '+';

				case ExpressionType.Subtract:
				case ExpressionType.SubtractChecked:
					return '-';

				case ExpressionType.Multiply:
				case ExpressionType.MultiplyChecked:
					return '*';

				case ExpressionType.Divide:
					return '/';

				case ExpressionType.Modulo:
					return '%';

				case ExpressionType.ExclusiveOr:
					return '^';

				case ExpressionType.And:
					return '&';

				case ExpressionType.Or:
					return '|';

				default:
					throw new NotSupportedException(string.Format("The binary operator '{0}' is not supported", b.NodeType));
			}
		}

		private Expression VisitOperation(BinaryExpression b)
		{
			this.Visit(b.Left);
			sb.Append(" " + GetOperator(b) + " ");
			this.Visit(b.Right);
			return b;
		}

		private Expression VisitCoalesce(BinaryExpression b)
		{
			sb.Append("coalesce(");
			this.Visit(b.Left);
			sb.Append(", ");
			this.Visit(b.Right);
			sb.Append(")");
			return b;
		}

		private Expression VisitComparison(BinaryExpression b)
		{
			sb.Append("(");
			this.Visit(b.Left);
			switch (b.NodeType)
			{
				case ExpressionType.AndAlso:
					sb.Append(" and ");
					break;
				case ExpressionType.OrElse:
					sb.Append(" or ");
					break;
				case ExpressionType.LessThan:
					sb.Append(" < ");
					break;
				case ExpressionType.LessThanOrEqual:
					sb.Append(" <= ");
					break;
				case ExpressionType.GreaterThan:
					sb.Append(" > ");
					break;
				case ExpressionType.GreaterThanOrEqual:
					sb.Append(" >= ");
					break;
				default:
					throw new NotSupportedException(string.Format("The binary operator '{0}' is not supported", b.NodeType));
			}
			this.Visit(b.Right);
			sb.Append(")");
			return b;
		}

		private Expression VisitEquality(BinaryExpression b)
		{
			sb.Append("(");
			if (b.Right.NodeType == ExpressionType.Constant &&
				(b.Right as ConstantExpression).Value == null)
			{
				this.Visit(b.Left);
				sb.Append(b.NodeType == ExpressionType.Equal ? " is null" : " is not null");
			}
			else if (b.Left.NodeType == ExpressionType.Constant &&
				(b.Left as ConstantExpression).Value == null)
			{
				this.Visit(b.Right);
				sb.Append(b.NodeType == ExpressionType.Equal ? " is null" : " is not null");
			}
			else
			{
				this.Visit(b.Left);
				sb.Append(b.NodeType == ExpressionType.Equal ? " = " : " <> ");
				this.Visit(b.Right);
			}
			sb.Append(")");
			return b;
		}

		protected override Expression VisitMemberAccess(MemberExpression m)
		{
			sb.Append(m.Member.Name);
			return m;
		}

		protected override Expression VisitMethodCall(MethodCallExpression m)
		{
			var method = m.Method;
			var args = m.Arguments.ToList();
			if (method.DeclaringType == typeof(Convert))
			{
				if (args.Count != 1)
					goto error;

				sb.Append("cast(");
				this.Visit(m.Arguments[0]);
				switch (method.Name)
				{
					case "ToBoolean":
						sb.Append(" as bit)");
						return m;
					case "ToChar":
						sb.Append(" as nchar)");
						return m;
					case "ToDateTime":
						sb.Append(" as datetime)");
						return m;
					case "ToByte":
					case "ToSByte":
						sb.Append(" as tinyint)");
						return m;
					case "ToInt16":
					case "ToUInt16":
						sb.Append(" as smallint)");
						return m;
					case "ToInt32":
					case "ToUInt32":
						sb.Append(" as int)");
						return m;
					case "ToInt64":
					case "ToUInt64":
						sb.Append(" as bigint)");
						return m;
					case "ToSingle":
						sb.Append(" as real)");
						return m;
					case "ToDouble":
						sb.Append(" as float)");
						return m;
					case "ToString":
						sb.Append(" as nvarchar)");
						return m;
					case "ToDecimal":
						sb.Append(" as decimal)");
						return m;
					default:
						goto error;
				}
			}
			else if (method.DeclaringType == typeof(Math))
			{
				switch (method.Name)
				{
					case "Round":
						if (args.Count < 3)
							throw new NotSupportedException(string.Format("The method call '{0}.{1}' is not supported with one or two arguments; MidpointRounding.ToEven or MidpointRounding.AwayFromZero must be specified.", method.DeclaringType, method.Name));
						sb.Append("round(");
						this.Visit(args[0]);
						sb.Append(", ");
						this.Visit(args[1]);
						if (args[2].NodeType != ExpressionType.Constant || args[2].Type != typeof(MidpointRounding))
							throw new NotSupportedException(string.Format("The method call '{0}.{1}' is not supported with one or two arguments; MidpointRounding.ToEven or MidpointRounding.AwayFromZero must be specified.", method.DeclaringType, method.Name));
						if (((MidpointRounding)(args[2] as ConstantExpression).Value) == MidpointRounding.ToEven)
						{
							// formula proven mathematically, validated empirically
							// (cast(val * power(10, prec) as int) % 2) * round((10 - (val * power(10, prec + 1) % 10)) / 5, 0, 1)
							sb.Append(", (cast(");
							this.Visit(args[0]);
							sb.Append(" * power(10, ");
							this.Visit(args[1]);
							sb.Append(") as int) % 2) * round((10 - (");
							this.Visit(args[0]);
							sb.Append(" * power(10, ");
							this.Visit(args[1]);
							sb.Append(" + 1) % 10)) / 5, 0, 1)");

						}
						sb.Append(")");
						return m;

					case "Abs":
					case "Asin":
					case "Acos":
					case "Atan":
					case "Ceiling":
					case "Cos":
					case "Exp":
					case "Floor":
					case "Sign":
					case "Sin":
					case "Sqrt":
					case "Tan":
						sb.Append(method.Name.ToLower());
						sb.Append("(");
						this.Visit(args[0]);
						sb.Append(")");
						return m;

					case "Pow":
						sb.Append("power(");
						this.Visit(args[0]);
						sb.Append(")");
						return m;

					case "Atan2":
						sb.Append("atn2(");
						this.Visit(args[0]);
						sb.Append(", ");
						this.Visit(args[1]);
						sb.Append(")");
						return m;

					case "Log":
					case "Log10":
						sb.Append(method.Name.ToLower());
						sb.Append("(");
						this.Visit(args[0]);
						if (args.Count > 1)
						{
							sb.Append(", ");
							this.Visit(args[1]);
						}
						sb.Append(")");
						return m;

					case "Truncate":
						sb.Append("round(");
						this.Visit(args[0]);
						sb.Append(", 0, 1)");
						return m;

					case "IEEERemainder":
						sb.Append("(");
						this.Visit(args[0]);
						sb.Append(" % ");
						this.Visit(args[1]);
						sb.Append(")");
						return m;

					default:
						goto error;
				}
			}

		error:
			throw new NotSupportedException(string.Format("The method call '{0}.{1}' is not supported.", method.DeclaringType, method.Name));
		}

		protected override Expression VisitConstant(ConstantExpression c)
		{
			this.WriteValue(c);
			return c;
		}

		protected virtual void WriteValue(ConstantExpression c)
		{
			var value = c.Value;
			if (value == null) sb.Append("null");
			else
			{
				switch (Type.GetTypeCode(value.GetType()))
				{
					case TypeCode.Boolean:
						sb.Append(((bool)value) ? '1' : '0');
						break;

					case TypeCode.Int16:
					case TypeCode.Int32:
					case TypeCode.Int64:
					case TypeCode.Single:
					case TypeCode.Double:
					case TypeCode.Decimal:
						sb.Append(value);
						break;

					case TypeCode.String:
						sb.AppendFormat("'{0}'", value.ToString());
						break;

					case TypeCode.Object:
						var q = value as IQueryable;
						if (q != null)
						{
							sb.Append(q.ElementType.Name);
							break;
						}
						else
							goto default;

					default:
						throw new NotSupportedException(string.Format("The constant for '{0}' is not supported.", value));

				}
			}
		}

		private string QuoteString(string name)
		{
			return string.Format("[{0}]", name);
		}
	}
}
